#Issue# Golang Microservice 工程开发踩坑记录

#Issue# Golang Microservice 工程开发踩坑记录

第一次独立开发 Golang 微服务工程的记录,方案设计具体开发细节对齐通通都被我踩了一遍坑(真有我的.jpg


目录 Table of Contents


方案

阶段产物

  • 目标功能:该微服务实现的功能简要说明
  • 模块范围:该微服务涉及的上下游及自身模块的业务范围
  • 逻辑架构:整体或内部的架构设计
  • 调用时序:程序工作的调用时序
  • 流程判断:复杂部分的流程判断
  • 接口设计:先出文档然后开发
  • 数据库模型设计:数据库表
  • 注意事项:一些技术细节或潜在影响因素

接口设计

遵循 RESTful 风格

充分考虑参数属性

  • 文本长度
    • Path:过长的参数不宜放在 path
    • Query:不同浏览器和服务器限制各异,默认为 2048 个字节
    • Header:若服务器不作限制,可认为无限制
    • Body:若服务器不作限制,可认为无限制
  • 特殊字符:只有 Path 和 Query 需要进行编码和解码
    • 上游:浏览器会自动 urlencode 编码,其它需要手工 urlencode 编码
    • 下游:一般服务器的 Web 框架已经做了 urldecode 处理
  • 经验参考
    • Token:Path 和 Query 对文本长度有一定限制,Body 不一定符合 RESTful 风格,放在 Header 是比较合适的;但页面跳转请求时,Header 无法生效,此时放在 Query 更加方便

数据库模型设计

字段属性

  • 主键
    • 字段应该唯一:存在重复不适合做主键
    • 字段建立索引:内容太长不适合做主键
  • 文本
    • 考虑长文本使用 varchar 和 text 的区别

更新方式

  • 原地更新
    • 优点:节省空间
    • 缺点:多入口的并发读写会互相影响
  • 插入更新
    • 优点:多入口的并发读写会互相独立
    • 缺点:浪费空间

索引建立

  • 若出现长文本或数据少的情景:
    • 长文本不建议建索引
    • 数据少可以不建索引

开发

加密方法

  • AES (CBC + PKCS7 + PBKDF2):对称加密算法,内部服务之间使用该加密方法,速度较快
  • RSA (PKCS1v15 for pub_key + PKCS8 for pri_key):非对称加密算法,外部服务交互使用该加密方法,保密较强

更新数据

  • 增量更新:先查询出原记录的结构体,若请求字段不为空则更新结构体并更新数据库
  • 全量更新:直接使用请求的结构体更新数据库

排查错误

  • 查看应用运行日志和数据库操作日志

异常处理

  • Golang 使用 err 代替 expection,err 包含的错误更广泛,当希望对特定某类错误进行处理时,要注意做区分。如:使用 gorm.ErrRecordNotFound 或判断列表是否为空来区分找不到和其它异常。

空值处理

  • Gin 的模型绑定中,当传入字段为空时结构体会赋默认值,希望忽略为空字段应该使用指针(https://github.com/gin-gonic/gin/issues/659)。
  • Gorm 的操作中,若对空值有所限制,需要注意是否会产生副作用。

使用公共库

  • 尽可能使用公共库,避免地重复造轮子

配置与常量

  • 配置:根据环境修改变量(改变时无侵入)
  • 常量:程序固有且基本不会改变(改变时须侵入)

中间件的使用

认证

1
2
3
4
5
6
7
8
9
10
11
// 可以直接在路由组应用中间件
secretGroup := router.Group(CONTEXT_PATH + "/secrets", gin.BasicAuth(gin.Accounts{
constants.BASIC_AUTH_USERNAME: constants.BASIC_AUTH_PASSWORD,
}))
{
secretGroup.GET("", controller.ListSecrets)
secretGroup.POST("", controller.CreateSecret)
secretGroup.GET("/:secret_id", controller.GetSecret)
secretGroup.PUT("/:secret_id", controller.UpdateSecret)
secretGroup.DELETE("/:secret_id", controller.DeleteSecret)
}

跨域

1
2
3
4
5
// 必须在路由注册前应用才会生效
config := cors.DefaultConfig()
config.AllowAllOrigins = true // 开放请求来源
config.AddAllowHeaders("customized_header") // 开放请求头部
router.Use(cors.New(config))

操作进行回滚

数据库层面

1
2
3
4
5
6
7
8
// 进行事务回滚
tx.Begin()
...
if success {
tx.Commit()
} else {
tx.Rollback()
}

外部调用层面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建失败后,删除脏数据
func Create(req *Request) error {
resource := createResource(req)

err := doSomethingElse()
if err != nil {
deleteResource(resource)
}
}

// 删除失败后,创建旧数据
func Delete(req *Request) error {
resource := getResource(req)
deleteResource(req)

err := doSomethingElse()
if err != nil {
createResource(resource)
}
}

对齐

接口出错的判定依据

  • 根据状态码还是错误码

同名参数的真实含义

  • 有些参数看似同名,实际上代表的意思在不同的微服务中是不一样的

附录

  1. 由前端登录验证,页面跳转,携带headers token引发的思考和尝试

  2. 服务端与客户端跳转的区别

  3. cookie 你咋还没整明白?

  4. 浏览器同源策略及Cookie的作用域

  5. 傻傻分不清之Cookie、Session、Token、JWT

  6. MySQL性能优化之char、varchar、text的区别

  7. 利用散列算法优化唯一索引性能(长文本字段的唯一索引优化)


Comments

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×